package com.zhz.common.exception.handler;


import cn.hutool.core.exceptions.ValidateException;
import com.zhz.common.enums.*;
import com.zhz.common.exception.BaseException;
import com.zhz.common.exception.BizException;
import com.zhz.common.i18n.UnifiedMessageSource;
import com.zhz.common.tool.mybatisplus.base.core.contract.R;
import com.zhz.common.tool.mybatisplus.base.dal.utils.JsonUtils;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * 自定义的公共异常处理器
 * 1.声明异常处理器
 * 2.对异常统一处理
 *
 * @author zhouhengzhe
 */
@Slf4j
@RestControllerAdvice
public class UnifiedExceptionHandler {
    /**
     * 生产环境
     */
    private final static String ENV_PROD = "prod";

    @Autowired
    private UnifiedMessageSource unifiedMessageSource;

    /**
     * 当前环境
     */
    @Value("${spring.profiles.active}")
    private String profile;

    private static String getUUID() {
        return UUID.randomUUID().toString().substring(24);
    }


    public UnifiedExceptionHandler() {
        log.info("### base init : UnifiedExceptionHandler ###");
    }

    /**
     * 获取国际化消息
     *
     * @param e 异常
     * @return
     */
    public String getMessage(BaseException e) {
        String code = "response." + e.getResponseEnum().toString();
        String message = unifiedMessageSource.getMessage(code, e.getArgs());
        if (message == null || message.isEmpty()) {
            return e.getMessage();
        }
        return message;
    }

    /**
     * 自定义异常
     *
     * @param e 异常
     * @return 异常结果
     */
    @ExceptionHandler(value = BaseException.class)
    public R<String> handleBaseException(HttpServletRequest request, BaseException e) {
        UnifiedExceptionHandler.RequestInfoDTO reqInfo = getRequestInfo(request, true);
        log.error("全局异常信息 ex={},req={}", e.getMessage(), reqInfo, e);
        return R.fail(e.getResponseEnum().getCode(), e.getMessage());
    }

    /**
     * Controller上一层相关异常
     *
     * @param e 异常
     * @return 异常结果
     */
    @ExceptionHandler({
            NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            HttpMediaTypeNotAcceptableException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    public R<String> handleServletException(Exception e) {
        log.error(e.getMessage(), e);
        int code = CommonResponseEnum.SERVER_ERROR.getCode();
        try {
            ServletResponseEnum servletExceptionEnum = ServletResponseEnum.valueOf(e.getClass().getSimpleName());
            code = servletExceptionEnum.getCode();
        } catch (IllegalArgumentException e1) {
            log.error("class [{}] not defined in enum {}", e.getClass().getName(), ServletResponseEnum.class.getName());
        }
        if (ENV_PROD.equals(profile)) {
            // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如404.
            code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return R.fail(code, message);
        }

        return R.fail(code, e.getMessage());
    }


    /**
     * 参数绑定异常
     *
     * @param e 异常
     * @return 异常结果
     */
    @ExceptionHandler(value = BindException.class)
    public R<String> handleBindException(BindException e) {
        log.error("参数绑定校验异常", e);
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(BizException.class)
    public R<String> bizException(HttpServletRequest request, BizException e) {
        UnifiedExceptionHandler.RequestInfoDTO reqInfo = getRequestInfo(request);
        String uuid = getUUID();
        log.error("{} 服务异常 {}", uuid, reqInfo, e);
        String message;
        if (StringUtils.isNotBlank(e.getMessage())) {
            message = e.getMessage();
        } else {
            message = e.getResponseEnum().getMessage();
        }
        return R.fail(ResultCode.FAIL.getCode(), message);
    }

    private static UnifiedExceptionHandler.RequestInfoDTO getRequestInfo(HttpServletRequest request) {
        return getRequestInfo(request, false);
    }


    @ExceptionHandler({ValidateException.class})
    public R<String> validatorException(HttpServletRequest request, ValidateException e) {
        UnifiedExceptionHandler.RequestInfoDTO reqInfo = getRequestInfo(request, true);
        log.error("参数检验错误，违反约束异常，异常信息为 {} {}", reqInfo, e.getMessage());
        return R.fail(ErrorCodeEnum.PARAMETER_VALIDATION_FAILED.getCode(), e.getMessage());
    }

    /**
     * 参数校验(Valid)异常，将校验失败的所有异常组合成一条错误信息
     *
     * @param e 异常
     * @return 异常结果
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R<String> handleValidException(MethodArgumentNotValidException e) {
        log.error("参数绑定校验异常", e);

        return wrapperBindingResult(e.getBindingResult());
    }

    /**
     * 包装绑定异常结果
     *
     * @param bindingResult 绑定结果
     * @return 异常结果
     */
    private R<String> wrapperBindingResult(BindingResult bindingResult) {
        StringBuilder msg = new StringBuilder();

        for (ObjectError error : bindingResult.getAllErrors()) {
            msg.append(", ");
            if (error instanceof FieldError) {
                msg.append(((FieldError) error).getField()).append(": ");
            }
            msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());

        }

        return R.fail(ArgumentResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
    }

    /**
     * 未定义异常
     *
     * @param e 异常
     * @return 异常结果
     */
    @ExceptionHandler(value = Exception.class)
    public R<String> handleException(Exception e) {
        log.error(e.getMessage(), e);

        if (ENV_PROD.equals(profile)) {
            // 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
            int code = CommonResponseEnum.SERVER_ERROR.getCode();
            BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
            String message = getMessage(baseException);
            return R.fail(code, message);
        }

        return R.fail(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
    }

    @Data
    @Builder
    private static class RequestInfoDTO {
        private String url;
        private Object params;
        private String ip;
        private Map<String, String> headerMap;
    }

    private static UnifiedExceptionHandler.RequestInfoDTO getRequestInfo(HttpServletRequest request, boolean withHeader) {
        Map<String, String> map = null;
        if (withHeader) {
            Enumeration<String> headerNames = request.getHeaderNames();
            map = new HashMap<>(16);

            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                map.put(name, request.getHeader(name));
            }
        }

        String params = "";
        if (Objects.equals(request.getMethod(), "GET")) {
            params = JsonUtils.toJson(request.getParameterMap());
        } else {
            params = (String) request.getAttribute("requestBodyMessage");
        }

        return UnifiedExceptionHandler.RequestInfoDTO.builder().url(request.getRequestURL().toString()).params(params).ip(request.getRemoteAddr()).headerMap(map).build();
    }
}
